07864 Cosenseで文字数を常時オーバーレイ表示するUser Scriptを作りました
No. 07864, by shio / 塩澤一洋 https://flic.kr/p/2rKoyqt https://live.staticflickr.com/65535/54968622603_3cfd4e1fd3_3k.jpg
常時文字数表示、とうとう完全実現しました。
安定しているし、速度低下も招かないと思います。カウント対象となる文字も精査しました。
https://flic.kr/p/2rKoXkx https://live.staticflickr.com/65535/54968699689_b84838ef38_3k.jpg
学生たちが司法試験の論述をするために、編集画面で書いている文字数が常時表示される必要があります。
司法試験の必須科目:30字×23行×8頁=5,520字
司法試験の選択科目:30字×23行×4頁=2,760字
司法試験予備試験:30字×23行×4頁=2,760字
今まで、どなたかが作ってくださったコード(User Script)をあちこちから拝借して導入していましたが、表示されたりされなかったり……。不安定なので、4つくらいのコードを入れて、どれか表示されればいいや、という程度に使ってきました。各コードが表示する文字数にも誤差がありました。
https://flic.kr/p/2rKoywL https://live.staticflickr.com/65535/54968622968_db1d7468ae_3k.jpg
そこで、使っているすべてのUserScriptを一つ一つ評価し、改善を加えていただけるようChatGPTに依頼しました。文字数表示以外の各種コードはすんなりOKだったのですが、文字数を常時表示するスクリプトはどれも問題が多いとの診断結果。ChatGPTがそれぞれの改良版を作ってくれるのですが、一つを除いてどれもうまく作動しない。
そこでいずれかを改修することは諦め、新たに作ることにしました。
編集画面の左下に常時表示するならコードはすでにきちんと動いていました。でも、その方法だと、長い文章の途中を編集している場合、現在の文字数を見るには下までスクロールする必要がある。
常時目に入るような表示を実現したい。これがなかなか手強い。
特に編集画面をスクロールしても消えないように常時表示するのが結構難しい。右側のメニュの羅列に加えようとすると、最初は表示されても編集中に消えてしまう。画面上のあちこち試し、最終的には画面右下にオーバーレイ表示するのが他の要素と干渉することなく常時表示できてベスト。
https://flic.kr/p/2rKnCnX https://live.staticflickr.com/65535/54968440801_2205731415_3k.jpg
またカウント対象とする文字も精査しました。
カウント対象から外す文字列
ブラケットの中にあるURL
ブラケット内で文字装飾に使われる記号
ブラケット自体
改行コード
カウント対象に含める文字列
ブラケット内であっても文章を記述している文字列
全角スペース
半角スペース
https://flic.kr/p/2rKi4Cx https://live.staticflickr.com/65535/54967550187_d117e88c7f_3k.jpg
Macでの表示は以下のようになります。
下記スクショの右下隅に表示されている「584 chars」が常時文字数表示。
https://gyazo.com/4edad95ef8645734ede853161eaf909b
スマホの画面でも目障りでない程度に右下に常時オーバーレイ表示されます。
https://flic.kr/p/2rKi4BR https://live.staticflickr.com/65535/54967550147_684b166f53_3k.jpg
作成して導入したコードを下方に貼ります。
するとちゃんと文字数も増えて「2,889 chars」になってます。
https://gyazo.com/340e4a742b08675c198c1e5251f2c522
以下が、今回ChatGPTとやり取りしながら作成したコードです。コメント行も残してあります。 どうぞお使いくださいませ。
文字数常時表示
code:script.js
(() => {
if (!window.scrapbox) return;
const fmt = n => new Intl.NumberFormat('en-US').format(n);
// overlay element(右下固定)
let wrapper = document.getElementById('__charCounterOverlay__');
if (!wrapper) {
wrapper = document.createElement('div');
wrapper.id = '__charCounterOverlay__';
wrapper.style.position = 'fixed';
wrapper.style.bottom = '24px'; // お好みの位置に調整可
wrapper.style.right = '10px';
wrapper.style.zIndex = '9999';
wrapper.style.fontSize = '12px';
wrapper.style.opacity = '0.8';
wrapper.style.padding = '3px 8px';
wrapper.style.borderRadius = '4px';
wrapper.style.background = 'rgba(0,0,0,0.08)';
wrapper.style.pointerEvents = 'none';
document.body.appendChild(wrapper);
}
// current page lines
const getLines = () => (scrapbox.Page && scrapbox.Page.lines) || null;
// calc characters with rules:
// - ブラケット内の URL は削除し、残り(例: 07864)は数える
// - ブラケット内の装飾記号 (* /) は除外
// - ブラケット文字 自体は常に除外
// - ブラケット外の URL はそのまま文字としてカウント
// - スペース(全角・半角)はカウント対象(改行は除外)
const calcChars = rawText => {
try {
let text = String(rawText);
text = text.replace(/\[(^\]*)\]/g, (_m, inner) => { const trimmed = inner.trim();
if (/^https?:\/\/\S+$/.test(trimmed)) return '';
let t = inner.replace(/https?:\/\/\S+/g, '');
t = t.replace(/*//g, ''); return t;
});
text = text.replace(/[\\]/g, ''); text = text.replace(/\r\n/g, ''); return text.length;
} catch (_e) {
return rawText ? String(rawText).length : 0;
}
};
// 実際の更新処理
const doUpdate = () => {
const lines = getLines();
if (!lines || !lines.length) {
wrapper.textContent = '';
return;
}
const text = lines.map(l => l.text).join('\n');
const chars = calcChars(text);
wrapper.textContent = ${fmt(chars)} chars;
};
// デバウンス用タイマー
let pending = null;
const scheduleUpdate = (delay) => {
if (pending) clearTimeout(pending);
pending = setTimeout(() => {
pending = null;
doUpdate();
}, delay);
};
// 初期表示(Page.lines が入るタイミングの揺らぎに備えて数回試す)
requestAnimationFrame(doUpdate);
setTimeout(doUpdate, 200);
setTimeout(doUpdate, 1000);
// events
if (typeof scrapbox.on === 'function') {
// ページ切替時はすぐ更新(ただし1ティック後)
scrapbox.on('page:changed', () => {
scheduleUpdate(0);
});
// 編集中は 150ms デバウンスしてから再計算
scrapbox.on('lines:changed', () => {
scheduleUpdate(150);
});
}
})();
何かお気づきの点がありましたら、ぜひご教示くださいませ。
https://flic.kr/p/2rKpcEL https://live.staticflickr.com/65535/54968747900_c834eb7de8_3k.jpg
https://gyazo.com/c8df74caca3d9181fc391a27f9150b54